LOADING...

加载过慢请开启缓存(浏览器默认开启)

loading

SpringCloud基础+电商实战

2024/4/11 后端

Spring Cloud基础介绍

  • 微服务的基本概念、设计与拆分原则
  • 微服务和Spring Cloud的关系
  • 微服务常见的组件和功能
  • 课程查询案例基本介绍、系统架构设计和接口设计
  • 分模块构建Spring Cloud项目
  • 完成课程列表、课程价格服务开发
  • 课程服务整合,服务注册与发现
  • 整合Feign实现服务间调用
  • 网关的集成与开发,并接入服务
  • 引入服务的熔断与降级,并进行实操演练

微服务基础

  • 什么是微服务?
  • 微服务的特点
  • 微服务优缺点
  • 微服务的两大门派 [Spring Cloud 和 Dubbo]
  • 微服务拆分
  • 微服务扩展
  • 微服务重要模块

微服务热度

单体应用的痛点

什么是服务化
  • 把传统的单机应用中的本地方法调用,改造成通过RPC、HTTP产生的远程方法调用
  • 把模块从单体应用中拆分出来,独立成一个服务部署
什么是微服务
  • 一系列、一部分

  • 是一种架构风格

  • 开发单体应用作为一系列小型服务的套件,其中每个服务都运行再自己的进程中,并且通过轻量级的机制实现彼此间的通信,这通常是HTTP资源API

  • 这些服务是围绕着业务功能构建的,并且可以通过完全自动化的部署机制进行独立部署

  • 这些服务的集中式管理做到了最小化(例如docker相关技术),每一种服务都可以通过不同的编程语言进行编写,并且可以使用不同的数据存储技术

微服务的特点

  • 组件以服务形式来提供

  • 产品不是项目

  • 轻量级通信、独立进程

  • 分散治理、去中心化治理

  • 容错性设计

  • 会带来团队组织架构的调整

微服务优缺点

  • 服务简单、便于学习和上手,相对易于维护
  • 独立部署,灵活扩展
  • 技术栈丰富
微服务缺点
  • 运维成本过高 [磁盘满 CPU高]
  • 接口可能不匹配
  • 代码可能重复 [要非常明确的定义每个API]
  • 架构复杂度提高

微服务两大门派

  • Spring Cloud:众多子项目【最大供应者:Netflix内容付费知识】
  • dubbo:高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现
  • dubbo提供的能力只是SpringCloud的一部分子集
[Dubbo虽然有很多不提供的组件 但是可以和其他供应商合作完成提供任务]
核心组件 Dubbo Spring Cloud
服务注册中心 Zookeeper Spring Cloud Netflix Eureka
服务调用方式 RPC REST API
服务网关 Spring Cloud Netflix Zuul
断路器 不完善 Spring Cloud Netflix Hystrix
分布式配置 Spring Cloud Config
服务跟踪 Spring Cloud Sleuth
消息总线 Spring Cloud Bus
数据流 Spring Cloud Stream
批量任务 Spring Cloud Task
通信协议对比
  • RPC vs REST
  • 服务提供方与调用方接口依赖方式太强
  • 服务对平台敏感,难以简单复用
文档质量对比
  • Dubbo的文档可以说再国内开源框架中算是一流的,提供了中文与英文两种版本
  • Spring Cloud文档体量大,更多的是偏向整合,更深的使用方法还是需要查看整合组件的详细文档
两大门派选型建议
  • Dubbo => 组装电脑 【中文文档多】
  • Spring Cloud => 品牌机【稳定可靠】

微服务拆分

什么时候进行服务化拆分
  • 第一阶段的主要目标是快速开发和验证想法
  • 进一步增加更多的新特性来吸引更多的目标用户
  • 同时进行开发的人员超过10人,这个时候就该考虑到服务化拆分了
不适合拆分的情况
  • 小团队,技术基础较薄弱
  • 流量不高,压力小,业务变化也不大
  • 对延迟很敏感的低延迟高并发系统
服务化拆分的两种方式
  • 纵向拆分[上方区别图]
  • 横向拆分
  • 结合业务综合分析

服务扩展

维度

自动按需扩展
  • 根据CPU负载程度、特定时间(比如周末)、消息中间件的队列长度、业务具体规则、预测等来决定是否扩展
  • 自动分配一个新的服务实例,提高可用性
  • 提高了可伸缩性(双11之后,自动减少服务器)
  • 具有最佳使用率,节约成本

微服务重要模块

  • 服务描述
  • 注册中心
  • 服务框架
  • 负载均衡
  • 熔断和降级
  • 网关

Spring Cloud课程查询

  • Spring Cloud简介
  • 项目整体设计
  • 课程列表模块开发
  • 课程价格模块开发 [模块间互相调用]
  • 服务注册与发现Eureka
  • 服务间调用Feign
  • 负载均衡Ribbon
  • 熔断器Hystrix [兜底界面 默认返回]
  • 网关Zuul
  • 整体测试

Spring Cloud简介

  • 成熟的微服务框架,定位为开发人员提供工具,以快速构建分布式系统
核心组件
核心组件 Spring Cloud
服务注册中心 Spring Cloud Netflix Eureka
服务调用方式 REST API、Feign、Ribbon
服务网关 Spring Cloud Netflix Zuul
熔断器 Spring Cloud Netflix Hystrix

项目整体设计

  • 项目介绍

  • 接口设计
    • 课程列表
    • 单个课程价格
    • 整合课程列表和价格
  • 表设计

系统数据流向

【课程列表数据】→ 【课程列表服务】

【课程价格数据】→ 【课程价格服务】→ 【整体列表+价格】

新建多模块项目

创建一个 spring-cloud-course-pracice 并删除src文件夹
新建New → module → Maven → course-service → 删除src
    新建New → module → New Module → Maven → course-list → Parent:course-service 
    新建New → module → New Module → Maven → course-price → Parent:course-service 
D:\Java+4399\阶段5:Java分布与微服务实战\第32周 Spring Cloud基础\第2节 Spring Cloud开发课程查询功能\辅助材料\SpringCloud课程查询源码【优质it资源微信it-wangke18】.zip\SpringCloud课程查询源码\课程价格模块开发后\spring-cloud-course-practice
course-service.course-list
pom.xml
    <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <parent>
    <artifactId>course-service</artifactId>
    <groupId>com.imooc</groupId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>

  <artifactId>course-list</artifactId>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>2.1.1</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>
com/imooc/course/controller/CourseListController.java
package com.imooc.course.controller;

import com.imooc.course.entity.Course;
import com.imooc.course.service.CourseListService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * CourseListController课程列表Controller
 */

@RestController
public class CourseListController {
    //提供课程列表服务
    @Autowired //引入课程服务
    CourseListService courseListService;


    @GetMapping("/courses")
    public List<Course> courseList() {
        //return → service
        return courseListService.getCourseList();
    }
}
com/imooc/course/service/CourseListService.java
package com.imooc.course.service;

import com.imooc.course.entity.Course;

import java.util.List;

/**
 * 课程列表服务
 */
public interface CourseListService {
    List<Course> getCourseList();
}
com/imooc/course/service/impl/CourseListServiceImpl.java
package com.imooc.course.service.impl;

import com.imooc.course.dao.CourseMapper;
import com.imooc.course.entity.Course;
import com.imooc.course.service.CourseListService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 课程服务实现类
 */
@Service
public class CourseListServiceImpl implements CourseListService {
    @Autowired
    CourseMapper courseMapper;
    @Override
    public List<Course> getCourseList() {
        return courseMapper.findValidCourses();
    }
}
com/imooc/course/entity/Course.java
package com.imooc.course.entity;

import java.io.Serializable;

/**
 * Course的实体类
 */
public class Course implements Serializable {
    Integer id;
    Integer courseId;
    String courseName;
    Integer valid;

    @Override
    public String toString() {
        return "Course{" +
                "id=" + id +
                ", courseId=" + courseId +
                ", courseName='" + courseName + '\'' +
                ", valid=" + valid +
                '}';
    }
    Getter+Setter  
}
com/imooc/course/dao/CourseMapper.java
package com.imooc.course.dao;

import com.imooc.course.entity.Course;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * 描述课程的Mapper类
 */
@Mapper
@Repository
public interface CourseMapper {
    @Select("SELECT * FROM course WHERE valid = 1")
    List<Course> findValidCourses();
}
application.properties

server.port=8081
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/course_practice?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
# entity的实体类与数据库名进行驼峰命名转换
mybatis.configuration.map-underscore-to-camel-case=true 
spring.application.name=course-list
com/imooc/course/CoursePriceApplication.java
package com.imooc.course;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CoursePriceApplication {

    public static void main(String[] args) {
        SpringApplication.run(CoursePriceApplication.class, args);
    }
}
=====================================================================
数据库名:course_practice
   表名:course
   字段:id[自增]   course_id   course_name   valid
课程列表模块开发-总结 [注意点]
  • 多模块开发
  • 实体类实现Serializable接口、set方法
  • MyBatis的驼峰配置
course-list.course-price
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <parent>
    <artifactId>course-service</artifactId>
    <groupId>com.imooc</groupId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>

  <artifactId>course-price</artifactId>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>2.1.1</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>
com/imooc/course/controller/CoursePriceController.java
package com.imooc.course.controller;

import com.imooc.course.entity.CoursePrice;
import com.imooc.course.service.CoursePriceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 课程价格控制器
 */

@RestController
public class CoursePriceController {
    @Autowired
    CoursePriceService coursePriceService;
    @GetMapping("/price")
    public Integer getCoursePrice(Integer courseId){
        CoursePrice coursePrice = coursePriceService.getCoursePrice(courseId);
        return coursePrice.getPrice();
    }
}
com/imooc/course/service/CoursePriceService.java
package com.imooc.course.service;

import com.imooc.course.entity.CoursePrice;

import java.util.List;

/**
 * 课程价格服务
 */
public interface CoursePriceService {
    CoursePrice getCoursePrice(Integer courseId);
}
com/imooc/course/service/impl/CoursepriceServiceImpl.java
package com.imooc.course.service.impl;

import com.imooc.course.dao.CoursePriceMapper;
import com.imooc.course.entity.CoursePrice;
import com.imooc.course.service.CoursePriceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * 课程价格的服务实现类
 */
@Service
public class CoursepriceServiceImpl implements CoursePriceService {
    @Autowired
    CoursePriceMapper coursePriceMapper;
    @Override
    public CoursePrice getCoursePrice(Integer courseId) {
        return coursePriceMapper.findCoursePrice(courseId);
    }
}
com/imooc/course/entity/CoursePrice.java
package com.imooc.course.entity;

import java.io.Serializable;

/**
 * CoursePrice的实体类
 */
public class CoursePrice implements Serializable {
    Integer id;
    Integer courseId;
    Integer price;

    @Override
    public String toString() {
        return "CoursePrice{" +
                "id=" + id +
                ", courseId=" + courseId +
                ", price=" + price +
                '}';
    }
    Getter+Setter
}
com/imooc/course/dao/CoursePriceMapper.java
package com.imooc.course.dao;

import com.imooc.course.entity.CoursePrice;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

/**
 * 课程价格Mapper类
 */
@Mapper
@Repository
public interface CoursePriceMapper {
    @Select("SELECT * FROM course_price WHERE course_id = #{courseId}")
    CoursePrice findCoursePrice(Integer courseId);
}
com/imooc/course/CoursePriceApplication.java
package com.imooc.course;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CoursePriceApplication {

    public static void main(String[] args) {
        SpringApplication.run(CoursePriceApplication.class, args);
    }
}
==========================================================
http://127.0.0.1:8082/price?courseId=409
数据库名:course_practice
   表名:course_price
   字段:id[自增]   course_id    price

Eureka的作用和架构

Eureka
  • 用于定位服务,直接找到组件中的各个服务地址
  • 114[各种服务的提供者]、物业[维护各个住户的信息(注册中心)]
为什么需要服务注册与发现 [移除不影响 但会有很多麻烦]
  • IP变化
  • 难以维护
  • 改进
    • 节点变化[服务的提供者和消费者] [消费者需要调用提供者的API来获得服务] 若提供者修改了ip 此时应该上传到注册中心,消费者此时需要得到IP和API 无需直接找提供者 直接去注册中心调用
Eureka架构
  • EureKa Server 和 EureKa Client

  • 集群 [只要能获得一个Eureka Server 就能获得整个信息]

引入Eureka

  • 引入依赖
  • 配置文件
  • 启动注解
pom.xml(eureka-server)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spring-cloud-course-practice</artifactId>
        <groupId>com.imooc</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>eureka-server</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <!-- 模块名及描述信息 -->
    <name>course-eureka-server</name>
    <description>Spring Cloud Eureka</description>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
pom.xml(Spring-cloud-course-practice)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <packaging>pom</packaging>
  <modules>
    <module>course-service</module>
    <module>eureka-server</module>
  </modules>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.12.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>com.imooc</groupId>
  <artifactId>spring-cloud-course-practice</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>spring-cloud-course-practice</name>
  <description>course project for Spring Cloud</description>

  <properties>
    <java.version>1.8</java.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <!--  表示Spring Cloud的版本-->
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>Greenwich.SR5</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

</project>
application.properties(eureka-server)

spring.application.name=eureka-server
server.port=8000
eureka.instance.hostname=localhost
#fetch-registry???????????????????
eureka.client.fetch-registry=false
#register-with-eureka??????????Eureka Server????true?
eureka.client.register-with-eureka=false
eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
application.properties(course-list)

server.port=8081
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/course_practice?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
# entity?????????????????
mybatis.configuration.map-underscore-to-camel-case=true 
spring.application.name=course-list
#??????? eureka-server??application.properties?defaultZone????????????????
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/
application.properties(course-price)

server.port=8082
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/course_practice?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
mybatis.configuration.map-underscore-to-camel-case=true
spring.application.name=course-price
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/
com/imooc/course/EurekaServerApplication.java
package com.imooc.course;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

/**
 * Eureka的服务端
 */
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}
========================================================
http://127.0.0.1:8000/

利用Feign实现服务间调用

Feign
  • 声明式、模板化的HTTP客户端,方便的调用远程的HTTP请求 [基于接口实现]
集成Feign
  • 引入依赖
  • 配置文件
  • 注解
pom.xml (course-price)
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>

application.properties
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/
com/imooc/course/controller/CoursePriceController.java
package com.imooc.course.controller;

import com.imooc.course.client.CourseListClient;
import com.imooc.course.entity.Course;
import com.imooc.course.entity.CoursePrice;
import com.imooc.course.service.CoursePriceService;
import java.util.List;
import javax.xml.ws.Action;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 描述:     课程价格控制器
 */
@RestController
public class CoursePriceController {

    @Autowired
    CoursePriceService coursePriceService;

    @Autowired
    CourseListClient courseListClient;

    @GetMapping("/price")
    public Integer getCoursePrice(Integer courseId) {
        CoursePrice coursePrice = coursePriceService.getCoursePrice(courseId);
        return coursePrice.getPrice();
    }

    @GetMapping("/coursesInPrice")
    public List<Course> getCourseListInPrice(Integer courseId) {
        List<Course> courses = courseListClient.courseList();
        return courses;
    }
}
com/imooc/course/client/CourseListClient.java[接口]
package com.imooc.course.client;

import com.imooc.course.entity.Course;
import java.util.List;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * 描述:     课程列表的Feign客户端
 */
@FeignClient("course-list")
public interface CourseListClient {

    @GetMapping("/courses")
    List<Course> courseList();
}
com/imooc/course/CoursePriceApplication.java
package com.imooc.course;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class CoursePriceApplication {

    public static void main(String[] args) {
        SpringApplication.run(CoursePriceApplication.class, args);
    }
}
=============================================================
http://127.0.0.1:8082/coursesInPrice

[
    {
        "id": 1,
        "courseId": 362,
        "courseName": "SpringCloud自学",
        "valid": 1
    },
    {
        "id": 2,
        "courseId": 409,
        "courseName": "玩转Java并发工具",
        "valid": 1
    }
]

负载均衡的两种类型

  • 客户端负载均衡(Ribbon)
  • 服务端负载均衡(Nginx)
负载均衡策略
  • RandomRule 表示随机策略
  • RoundRobinRule 表示轮询策略
  • ResponseTimeWeightedRule加权,根据每一个Server的平均响应时间动态加权
配置不同的负载均衡方式
  • Ribbon.NFLoadBalancerRuleClassName
application.properties(course-price)
course-list.ribbon.NFLoadBanlancerRuleClassName=com.netflix.loadbalancer.RoundRobinRule

为什么要断路器

Hystrix
pom.xml(course-price)   
<dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
application.properties(course-price)
feign.hystrix.enabled=true
com/imooc/course/CoursePriceApplication.java
package com.imooc.course;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
@EnableCircuitBreaker
public class CoursePriceApplication {

    public static void main(String[] args) {
        SpringApplication.run(CoursePriceApplication.class, args);
    }
}
================================================================
所有服务都打开时 http://127.0.0.1:8082/coursesInPrice
[
    {
        "id": 1,
        "courseId": 362,
        "courseName": "SpringCloud白学",
        "valid": 1
    },
    {
        "id": 2,
        "courseId": 409,
        "courseName": "玩转Java并发工具",
        "valid": 1
    }
]
当把CouresListApplication服务关闭的时候
[
    {
        "id": 1,
        "courseId": 1,
        "courseName": "默认课程",
        "valid": 1
    }
]

整合两个服务

com/imooc/course/controller/CoursePriceController.java
    @GetMapping("/coursesAndPrice")
    public List<CourseAndPrice> getCoursesAndPrice(){
        List<CourseAndPrice> courseAndPrices = coursePriceService.getCourseAndPrice();
        return courseAndPrices;
    }
=====================================================
http://127.0.0.1:8082/coursesAndPrice
[
    {
        "id": 1,
        "courseId": 362,
        "name": "SpringCloud自学",
        "price": 348
    },
    {
        "id": 2,
        "courseId": 409,
        "name": "玩转Java并发工具",
        "price": 399
    }
]
com/imooc/course/service/CoursePriceService.java
public interface CoursePriceService {

    CoursePrice getCoursePrice(Integer courseId);
    List<CourseAndPrice> getCourseAndPrice();
}
com/imooc/course/service/impl/CoursePriceServiceImpl.java
package com.imooc.course.service.impl;

import com.imooc.course.client.CourseListClient;
import com.imooc.course.dao.CoursePriceMapper;
import com.imooc.course.entity.Course;
import com.imooc.course.entity.CourseAndPrice;
import com.imooc.course.entity.CoursePrice;
import com.imooc.course.service.CoursePriceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * 描述:     课程价格的服务实现类
 */
@Service
public class CoursePriceServiceImpl implements CoursePriceService {

    @Autowired
    CoursePriceMapper coursePriceMapper;
    @Autowired
    CourseListClient courseListClient;

    @Override
    public CoursePrice getCoursePrice(Integer courseId) {
        return coursePriceMapper.findCoursePrice(courseId);
    }

    @Override
    public List<CourseAndPrice> getCourseAndPrice() {
        List<CourseAndPrice> courseAndPrices = new ArrayList<>();
        List<Course> courses = courseListClient.courseList();
        for (int i = 0; i < courses.size(); i++) {
            Course course = courses.get(i);
            //inn
            if (course != null) {
                CoursePrice coursePrice = getCoursePrice(course.getCourseId());
                CourseAndPrice courseAndPrice = new CourseAndPrice();
                courseAndPrice.setPrice(coursePrice.getPrice());
                courseAndPrice.setName(course.getCourseName());
                courseAndPrice.setId(coursePrice.getId());
                courseAndPrice.setCourseId(course.getCourseId());
                courseAndPrices.add(courseAndPrice);
            }
        }
        return courseAndPrices;
    }
}
com/imooc/course/entity/CourseAndPrice.java
package com.imooc.course.entity;

/**
 * 课程与价格的融合类
 */
public class CourseAndPrice {
    Integer id;
    Integer courseId;
    String name;
    Integer price;
}

网关Zuul

  • 为什么需要网关
  • 签名校验、登录校验冗余问题
  • Spring Cloud Zuul 与 Spring Cloud
  • API网关允许您将API请求(内部或外部)路由到正确的位置

集成Zuul [统一修改访问url地址]

  • 把自己注册到Eureka这个注册中心
  • 引入依赖
  • 配置路由地址
pom.xml(course-zuul)
     <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
    </dependencies>
com/imooc/course/ZuulGatewayApplication.java
package com.imooc.course;

import org.springframework.boot.SpringApplication;

/**
 * 网关启动类
 */
@EnableZuulProxy
@SpringCloudApplication
public class ZuulGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulGatewayApplication.class, args);
    }
}
===============================================================
http://127.0.0.1:9000/imooc/price/coursesInPrice
[
    {
        "id": 1,
        "courseId": 362,
        "courseName": "SpringCloud自学",
        "valid": 1
    },
    {
        "id": 2,
        "courseId": 409,
        "courseName": "玩转Java并发工具",
        "valid": 1
    }
]
application.properties

spring.application.name=course-gateway
server.port=9000
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
mybatis.configuration.map-underscore-to-camel-case=true
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/

zuul.prefix=/imooc
zuul.routes.course-list.path=/list/**
zuul.routes.course-list.service-id=course-list
zuul.routes.course-price.path=/price/**
zuul.routes.course-price.service-id=course-price

利用网关实现过滤器

  • pre 过滤器在路由请求之前运行
  • route 过滤器可以处理请求的实际路由
  • post 路由请求后运行过滤器
  • error 如果在处理请求的过程中发生错误,则过滤器将运行
com/imooc/course/filter/PreRequestFilter.java
package com.imooc.course.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

/**
 * 记录请求时间
 */
@Component
public class PreRequestFilter extends ZuulFilter {

    @Override
    public String filterType() {
        //过滤器的类型
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        //是否启用过滤器
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        //通过时间戳 获取上下文
        RequestContext currentContext = RequestContext.getCurrentContext();
        currentContext.set("startTime",System.currentTimeMillis());
        System.out.println("过滤器已经记录时间");
        return null;
    }
}
com/imooc/course/filter/PostRequestFilter.java
package com.imooc.course.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

/**
 * 请求处理后的过滤器
 */
@Component //有了才能被spring捕捉到
public class PostRequestFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.POST_TYPE;
    }

    @Override
    public int filterOrder() { //在其之前-1 先运行
        return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        return false;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext currentContext = RequestContext.getCurrentContext();
        Long startTime = (Long) currentContext.get("startTime");
        long duration = System.currentTimeMillis() - startTime;
        String requestURI = currentContext.getRequest().getRequestURI();
        System.out.println("uri:" + requestURI + ",处理时长:" + duration);
        return null;
    }
}


Spring Cloud电商实践

  • 服务拆分过程分析和经验分享
  • 完成用户、商品、购物车和订单等服务开发
  • 通用common模块的拆分和应用
  • Eureka server注册中心开发,用Feign完成服务之间的调用
  • 共享Session的处理方案
  • 统一网关的集成与开发

Spring Cloud电商项目

  • 项目介绍
    • 在Spring Boot的基础上升级为Spring Cloud
    • 从0到1 (Spring Boot)
    • 从1到多、微服务
  • 模块拆分
  • Eureka-server开发
  • 用户模块开发
  • 公共模块开发
  • 网关模块开发
  • 商品分类和商品模块开发
  • 购物车和订单模块开发
  • 总结

模块拆分

  • 粒度:过粗、适中、过细

  • 人员的角度

  • 业务的角度:相关、独立

  • Eureka-server模块

  • 网关模块

  • 公共模块 (md5、统一API返回、常量、异常、异常枚举、二维码、工具类模块……)

  • 用户模块 (相对来说比较独立,登录的时候无需获得商品信息)

  • 商品分类和商品模块

  • 购物车和订单模块

功能模块介绍

  • 项目功能:

    • 前台  {用户、商品分类、商品信息、购物车、订单}
      • 用户模块{注册、登录、更新签名、身份认证、登出}
      • 商品分类模块{多级目录、递归查询、缓存}
      • 商品模块{商品搜索、商品排序、商品列表、目录展示、商品详情}
      • 购物车模块{加入商品、列表显示、数量更改、删除商品、勾选反选、全选全不选}
      • 订单模块{下单、订单流程、订单详情、取消订单、支付二维码、扫码支付、个人订单、确认收货}
    • 后台  {用户、商品分类、商品信息、订单}
      • 管理员模块{登录登出、身份认证、安全限制}
      • 商品分类模块{分类列表、增加分类、修改分类、删除分类}
      • 商品模块{商品列表、新增商品、图片上传、更新删除、批量上下架}
      • 订单模块{订单列表、地址信息、发货、订单完结}

``

项目初始化

Eureka-server模块开发
  • 引入依赖
  • 配置文件
  • 启动注解

由于是多模块开发,创建了maven项目后把cloud-mall-practice的src删除

pom.xml(cloud-mall-practice)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.imooc</groupId>
    <artifactId>cloud-mall-practice</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>cloud-mall-eureka-server</module>
    </modules>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.SR5</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.7</version>
                <configuration>
                    <verbose>true</verbose>
                    <overwrite>true</overwrite>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
cloud-mall-eureka-server/com/imooc/cloud/mall/practice/eureka/EurekaServerApplication.java
package com.imooc.cloud.mall.practice.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

/**
 * 1. Eureka Server的启动类,提供服务注册与发现
 * 2写其pom和resources的配置文件
 *
 */
@EnableEurekaServer //对外提供服务
@SpringBootApplication
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}
pom.xml(cloud-mall-eureka-server)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>cloud-mall-practice</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>cloud-mall-eureka-server</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <name>
        cloud-mall-eureka-server
    </name>
    <description>Spring cloud Eureka Server</description>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
application.properties

spring.application.name=eureka-server
server.port=8000
eureka.instance.hostname=localhost
#是否同步其他节点的信息
eureka.client.fetch-registry=false
#是否把自己作为服务注册在服务上
eureka.client.register-with-eureka=false
#eureka-server所在的地址
eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/

用户模块知识点

登录、注册[加盐md5]、重名校验[注册就不可注册]、密码加密存储、Session的使用、越权校验[不可编辑别人的签名]

用户模块
  • 表设计
  • 开发
  • 测试

idea中如何将包名折叠或者或如何将折叠的包名展开_idea包名折叠-CSDN博客

用户模块初始化

公共模块
  • 常量、异常、工具类
  • 自身不是Spring Boot项目
进行模块各层级的重构 + 用户模块的测试
一定要记得如果引用其他muder的时候 要在xml中假如其项目文件的依赖才可以跨项目引用
比如我这个项目是【一定要引用噢!! 不然在非其项目的时候找不到import导包】
pom.xml(cloud-mall-zuul)
           <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>cloud-mall-user</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>

网关模块开发

pom.xml(cloud-mall-zuul)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>cloud-mall-practice</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>cloud-mall-zuul</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
application.properties(cloud-mall-zuul)
server.port=8083

spring.datasource.name=imooc_mall_datasource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/imooc_mall?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root

mybatis.mapper-locations=classpath*:mappers/*.xml

logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}

eureka.client.service-url.defaultZone=http://localhost:8000/eureka/

spring.application.name=cloud-mall-zuul

zuul.prefix=/
#凡是用户模块都要走/user地址
zuul.routes.cloud-mall-user.path=/user/**
#模块的名字
zuul.routes.cloud-mall-user.service-id=cloud-mall-user
com/imooc/cloud/mall/practice/zuul/filter/UserFilter.java
package com.imooc.cloud.mall.practice.zuul.filter;

import com.imooc.cloud.mall.practice.common.common.Constant;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.imooc.cloud.mall.practice.user.model.pojo.User;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
 * 用户鉴权过滤器
 */
@Component
public class UserFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        String requestURI = request.getRequestURI();
        //不经过过滤器
        if (requestURI.contains("images") || requestURI.contains("pay")) {
            return false;
        }
        if (requestURI.contains("cart") || requestURI.contains("order")) {//前置条件必须要登录!
            return true;
        }
        return false;
    }

    @Override
    //用户过滤器
    public Object run() throws ZuulException {//返回true的时候执行的
        RequestContext currentContext = RequestContext.getCurrentContext();//获取request
        //获取session 因为user对象保存在里面  session.getAttribute取出session对象
        HttpServletRequest request = currentContext.getRequest();
        HttpSession session = request.getSession();
        //拿出User对象
        User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        if (currentUser == null) {
            //无需通过网关再去发送
            currentContext.setSendZuulResponse(false);
            //返回给前端的对象
            currentContext.setResponseBody("{\n"
                    + "    \"status\": 10007,\n"
                    + "    \"msg\": \"NEED_LOGIN\",\n"
                    + "    \"data\": null\n"
                    + "}");
            currentContext.setResponseStatusCode(200);
        }
        return null;
    }
}
com/imooc/cloud/mall/practice/zuul/filter/AdminFilter.java
package com.imooc.cloud.mall.practice.zuul.filter;

import com.imooc.cloud.mall.practice.common.common.Constant;
import com.imooc.cloud.mall.practice.user.model.pojo.User;
import com.imooc.cloud.mall.practice.zuul.feign.UserFeignClient;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
 * 管理员鉴权过滤器
 */
@Component
public class AdminFilter extends ZuulFilter {
    @Autowired
    UserFeignClient userFeignClient;

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        String requestURI = request.getRequestURI();
        //不经过过滤器
        if (requestURI.contains("adminLogin")){
            return false;
        }
        if (requestURI.contains("admin")){
            return true;
        }
        if (requestURI.contains("cart") || requestURI.contains("order")) {//前置条件必须要登录!
            return true;
        }
        return false;
    }

    @Override
    //用户过滤器
    public Object run() throws ZuulException {//返回true的时候执行的
        RequestContext currentContext = RequestContext.getCurrentContext();//获取request
        //获取session 因为user对象保存在里面  session.getAttribute取出session对象
        HttpServletRequest request = currentContext.getRequest();
        HttpSession session = request.getSession();
        //拿出User对象
        User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        if (currentUser == null) {
            //无需通过网关再去发送
            currentContext.setSendZuulResponse(false);
            //返回给前端的对象
            currentContext.setResponseBody("{\n"
                    + "    \"status\": 10010,\n"
                    + "    \"msg\": \"NEED_LOGIN\",\n"
                    + "    \"data\": null\n"
                    + "}");
            currentContext.setResponseStatusCode(200);
            return null; //程序可以停止了
        }
        //进一步判断是否是管理员
        Boolean adminRole = userFeignClient.checkAdminRole(currentUser);
        if (!adminRole){
            currentContext.setSendZuulResponse(false);
            //返回给前端的对象
            currentContext.setResponseBody("{\n"
                    + "    \"status\": 10011,\n"
                    + "    \"msg\": \"NEED_ADMIN\",\n"
                    + "    \"data\": null\n"
                    + "}");
            currentContext.setResponseStatusCode(200);
        }
        return null;
    }
}
com/imooc/cloud/mall/practice/zuul/feign/UserFeignClient.java
package com.imooc.cloud.mall.practice.zuul.feign;

import com.imooc.cloud.mall.practice.user.model.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

/**
 * UserFeignClient
 */
@FeignClient(value = "cloud-mall-user")
public interface UserFeignClient {
    @PostMapping("/checkAdminRole")
    public Boolean checkAdminRole(@RequestBody User user);
}
com/imooc/cloud/mall/practice/zuul/ZuulGatewayApplication.java
网关模块加上SpringBoot启动类

package com.imooc.cloud.mall.practice.zuul;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * 网关启动类
 */
@EnableZuulProxy
@EnableFeignClients
@SpringBootApplication
public class ZuulGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulGatewayApplication.class, args);
    }
}
=================================================================
127.0.0.1:8083/user/login?userName=mumu9&password=lihao123
{
    "status": 10000,
    "msg": "SUCCESS",
    "data": {
        "id": 21,
        "username": "mumu9",
        "password": null,
        "personalizedSignature": "乘风破浪的姐姐",
        "role": 2,
        "createTime": "2024-04-17T18:08:15.000+0000",
        "updateTime": "2024-04-17T18:10:49.000+0000"
    }
}

POSTMAN中cloud-mall-practice的注册新用户、用户登录、管理员登录、登出模块都通过网关共用地址的调用,但是更新个性签名不可以,提示需要登录,因为更新签名需要提前登录。但是问题的最主要的点就是没有拿到session所以并没有把用户信息传到个性签名中 这是就需要Session共享机制

Session共享机制

登录功能分析
  • 登录状态需要保持
  • session的实现方案:登陆后,会保存用户信息到session
  • 之后的访问,先从session中获取用户信息,然后再执行业务逻辑
目前遇到的障碍 [记得去电脑端启动Redis]
  • session被网关过滤
  • 共享session [现在是多模块项目 其他项目保存了session 另一个项目无法获得]
  • EnableRedisHttpSession[需要通过中介Redis去调取]
在cloud-mall-user/application.properties中
server.port=8081

spring.datasource.name=imooc_mall_datasource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/imooc_mall?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root

mybatis.mapper-locations=classpath*:mappers/*.xml

logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}

eureka.client.service-url.defaultZone=http://localhost:8000/eureka/

spring.application.name=cloud-mall-user

spring.session.store-type=redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
在cloud-mall-zuul/application.properties中
server.port=8083

spring.datasource.name=imooc_mall_datasource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/imooc_mall?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root

mybatis.mapper-locations=classpath*:mappers/*.xml

logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}

eureka.client.service-url.defaultZone=http://localhost:8000/eureka/

spring.application.name=cloud-mall-zuul

spring.session.store-type=redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=

#没有敏感的需要过滤
zuul.sensitive-headers=
zuul.host.connect-timeout-millis=15000
zuul.prefix=/
#凡是用户模块都要走/user地址
zuul.routes.cloud-mall-user.path=/user/**
#模块的名字
zuul.routes.cloud-mall-user.service-id=cloud-mall-user
之后在UserApplication和ZuulGatewayApplication前面加上@EnableRedisHttpSession

com/imooc/cloud/mall/practice/user/UserApplication.java
package com.imooc.cloud.mall.practice.user;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
 * 启动类
 */
@SpringBootApplication
@EnableSwagger2
@MapperScan(basePackages = "com.imooc.cloud.mall.practice.user.model.dao")
@EnableRedisHttpSession
public class UserApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserApplication.class, args);
    }
}

-------------------------------------------------------------------------------------

com/imooc/cloud/mall/practice/zuul/ZuulGatewayApplication.java
package com.imooc.cloud.mall.practice.zuul;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

/**
 * 网关启动类
 */
@EnableZuulProxy
@EnableFeignClients
@SpringBootApplication
@EnableRedisHttpSession
public class ZuulGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulGatewayApplication.class, args);
    }
}

商品分类与商品模块初始化

什么是商品分类
  • 条理清楚,层次分明
  • 方便用户进行筛选和辨别
  • 可以通过分类的设置快速的进入对应的商品列表页面进行商品选择
分类层级
  • 在商品分类上需要继续做归类操作
  • 分类设置成三级
  • 层级太深的弊端:
    • 一是对用户不太友好,不利于寻找
    • 二是对于后台管理人员不友好,不方便管理

[cloud-mall-category-product]创造出来,将controller,model(dao,pojo),service,impi 里转入 Category来搞 因为在网关项目里写了关于管理员校验的方法 所以在此次处CategoryController把session里的校验删除

要让商品和商品目录用到User类 一定要去pom文件里添加依赖 不然idea找不到
pom.xml(cloud-mall-category-product)
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>cloud-mall-user</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
com/imooc/cloud/mall/practice/categoryproduct/config/CachingConfig.java
创建处理缓存的配置类 configpackage com.imooc.cloud.mall.practice.categoryproduct.config;

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;

import java.time.Duration;

*/**
* ** 57.缓存的配置类 想要运行成功保存序列化 要去弄个序列化接口
*   CategoryVO implements Serializable
* **/
*@Configuration
@EnableCaching
public class  CachingConfig {
    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {

        RedisCacheWriter redisCacheWriter = RedisCacheWriter
                .lockingRedisCacheWriter(connectionFactory);
        RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
        cacheConfiguration = cacheConfiguration.entryTtl(Duration.ofSeconds(30));

        RedisCacheManager redisCacheManager = new RedisCacheManager(redisCacheWriter,
                cacheConfiguration);
        return redisCacheManager;
    }
}
在网关中增加path和service-id
zuul.routes.cloud-mall-user.path=/user/**
zuul.routes.cloud-mall-user.service-id=cloud-mall-user

zuul.routes.cloud-mall-category-product.path=/category-product/**
zuul.routes.cloud-mall-category-product.service-id=cloud-mall-category-product
查端口  => netstat -ano | findstr :8083 [pid为7812]
杀死端口PID => taskkill /PID 7812 /f

商品模块

更新和新增商品
  • 合并写法不可取
  • 业务逻辑清晰、独立
批量上下架
  • MyBatis遍历List
  • where语句拼接
商品列表:搜索功能
入参判空 → 加%通配符 → like关键字
对于查询目录的in处理
  • 目录处理:如果查某个目录下的商品,不仅是需要查出来该目录的,还需要查出来子目录的所有商品
  • 这里要拿到某一个目录Id下的所有子目录id的List

前台:商品列表

  • 排序功能
  • Mybatis PageHelper
  • 枚举:order by
把所有product的java都移动过去 controller/service/impl/modal.dao/modal.pojo/query.ProductListQuery/request.AddProductReq、ProductListReq、UpdateProductReq/resourcs.mappers.ProductMapper.xml
将Constant里的@Value("${file.upload.dir}")的文件上传目录重构到category-product项目中
然后在其项目中的resources的application.properties中写一下路径
文件地址映射:config/imoocMallWebMvcConfig

图片端口的特殊处理

application.properties(cloud-mall-category-product)
file.upload.dir=/Users/Pluminary/Desktop/idea_Space/imooc-mall-prepare-static/

不能再通过获取getHost来配置了 根据实际情况去配置 没有办法获取到网关对外暴露的真正端口号
 private URI getHost(URI uri){
        URI effectiveURI;
        try {
            effectiveURI = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(),null,null,null);
        } catch (URISyntaxException e) {
            effectiveURI = null; //如果新建失败 就返回回去
        }
        return effectiveURI;
    }

要改为com/imooc/cloud/mall/practice/categoryproduct/controller/ProductAdminController.java
/**
 * 58.后台商品管理Controller  pojo的product复制一份到request变成AddProductReq  59.需要ProductService.java
 */
@RestController
public class ProductAdminController {

    @Autowired
    ProductService productService;

    @Value("${file.upload.ip}")
    String ip;

    @Value("${file.upload.port}")
    Integer port;

    @PostMapping("/admin/product/add")
    public ApiRestResponse addProduct(@Valid @RequestBody AddProductReq addProductReq) {
        productService.add(addProductReq);
        return ApiRestResponse.success();
    }

    @PostMapping("/admin/upload/file")
    public ApiRestResponse upload(HttpServletRequest httpServletRequest,
                                  @RequestParam("file") MultipartFile file) {
        String fileName = file.getOriginalFilename();
        String suffixName = fileName.substring(fileName.lastIndexOf("."));
        //生成文件名称UUID
        UUID uuid = UUID.randomUUID();
        String newFileName = uuid.toString() + suffixName;
        //创建文件
        File fileDirectory = new File(ProductConstant.FILE_UPLOAD_DIR);
        File destFile = new File(ProductConstant.FILE_UPLOAD_DIR + newFileName);
        if (!fileDirectory.exists()) {
            if (!fileDirectory.mkdir()) {
                throw new ImoocMallException(ImoocMallExceptionEnum.MKDIR_FAILED);
            }
        }
        try {
            file.transferTo(destFile);
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            return ApiRestResponse
                    .success(getHost(new URI(httpServletRequest.getRequestURL() + "")) + "/category-product/images/"
                            + newFileName);
        } catch (URISyntaxException e) {
            return ApiRestResponse.error(ImoocMallExceptionEnum.UPLOAD_FAILED);
        }
    }

    private URI getHost(URI uri) {
        URI effectiveURI;
        try {
            effectiveURI = new URI(uri.getScheme(), uri.getUserInfo(), ip, port,
                    null, null, null);
        } catch (URISyntaxException e) {
            effectiveURI = null;
        }
        return effectiveURI;
    }

阶段性重难点和常见错误

阶段总结
  • 重难点:模块拆分设计、公共模块、Zuul(网关)过滤器、Session处理、Feign调用
    Session在微服务的情况下就不容易处理到了可以共享Session,把其放在Redis中实现共享
    Feign模块之间的接口调用[HTTP是手动调用]
  • 常见错误:模块粒度不合适、无公共模块、各接口独立校验、session无法共享、HTTP手动调用
模块拆分
  • 粒度:过粗、适中、过细
  • 人员的角度
  • 业务的角度:相关、独立

购物车与订单模块

购物车模块
  • 添加商品到购物车 → 商品是否在售、是否有库存

    • →[否] 提示用户

    • →[是] 该商品之前就在购物车里

      • →[否] 添加新商品
      • →[是] 原有基础上添加数量
创建一个module 加入关于cart的controller/model.dao.pojo.vo/service.impl
其中ProductMapper productMapper会爆红是理所应当的 这样证明耦合不是很严重
利用远程调用Feign进行调用
思路:通过调查发现productMapper只用于挑选ID
Product product = productMapper.selectByPrimaryKey(productId);
所以可以去重构一下代码 回到商品模块的地方cloud-mall-category-product
其中controller中的ProductController
//com/imooc/cloud/mall/practice/categoryproduct/controller/ProductController.java
//这个是服务与服务之间的内部调用 不需要层层包装 只需要返回就好
    @GetMapping("product/detailForFeign")
    public Product detailForFeign(@RequestParam Integer id){
       Product product = productService.detail(id);
       return product;
    }
//千万不要直接引用另一个项目的mapper 因为耦合有点严重 只要对面发生变化 就完蛋了
//com/imooc/cloud/mall/practice/cartorder/feign/ProductFeignClient.java
package com.imooc.cloud.mall.practice.cartorder.feign;

import com.imooc.cloud.mall.practice.categoryproduct.model.pojo.Product;
import org.springframework.web.bind.annotation.RequestParam;

public interface ProductFeignClient {
    Product detailForFeign(@RequestParam Integer id);
}

用户模块提供获取当前用户接口

com/imooc/cloud/mall/practice/user/controller/UserController.java
 @GetMapping("/getUser")
    @ResponseBody
    public User getUser(HttpSession session){
        User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        return currentUser;
    } /**
     * 获取当前登录的User对象 为了避免暴露用户信息,可以再filter中对getUser进行拦截
     * @param session
     * @return
     */
    @GetMapping("/getUser")
    @ResponseBody
    public User getUser(HttpSession session){
        User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        return currentUser;
    }
com/imooc/cloud/mall/practice/cartorder/feign/UserFeignClient.java
package com.imooc.cloud.mall.practice.cartorder.feign;

import com.imooc.cloud.mall.practice.user.model.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * UserFeign客户端
 */
@FeignClient(value = "cloud-mall-user")
public interface UserFeignClient {
    /**
     * 获取当前登录的user对象
     */
    @GetMapping("/getUser")
    User getUser();
}
修改CaetMapper.xml的路径名
com/imooc/cloud/mall/practice/cartorder/feign/ProductFeignClient.java
package com.imooc.cloud.mall.practice.cartorder.feign;

import com.imooc.cloud.mall.practice.cartorder.pojo.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
 * 描述:     商品FeignClient
 */
@FeignClient(value = "cloud-mall-category-product")
public interface ProductFeignClient {
    //不经过网关 只是内部调用
    @GetMapping("product/detailForFeign")
    Product detailForFeign(@RequestParam Integer id);

    @PostMapping("product/updateStock")
    void updateStock(@RequestParam Integer productId, @RequestParam Integer stock);

}
将新增模块加入到网关[前缀地址] zuul
zuul.routes.cloud-mall-user.path=/user/**
zuul.routes.cloud-mall-user.service-id=cloud-mall-user
zuul.routes.cloud-mall-category-product.path=/category-product/**
zuul.routes.cloud-mall-category-product.service-id=cloud-mall-category-product
zuul.routes.cloud-mall-cart-order.path=/cart-order/**
zuul.routes.cloud-mall-cart-order.service-id=cloud-mall-cart-order

让Feign携带Session信息

错误新消息:500 Internal Server Error
在购物车中去调用User的getUser方法 是经过Feign
但是Feign的调用不经过网关 它是一个HTTP的调用
需要携带网关的Session信息

  • FeignRequestInterceptor [对每一个发出的Feign进行拦截]
    把网关的所有信息都复制到Feign的请求上 就不会遗漏相关信息
com/imooc/cloud/mall/practice/cartorder/filter/FeignRequestInterceptor.java
package com.imooc.cloud.mall.practice.cartorder.filter;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import java.util.Enumeration;
import javax.servlet.http.HttpServletRequest;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

/**
 * 描述:     Feign请求拦截器
 */
@EnableFeignClients
@Configuration
public class FeignRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        //通过RequestContextHolder获取到请求 拿到requestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes == null) {
            return;
        }//类型转换 拿到Request
        HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
        Enumeration<String> headerNames = request.getHeaderNames(); //拿到所有Header名字
        if (headerNames != null) {
            while (headerNames.hasMoreElements()) { //如果还有新的元素 先获取
                String name = headerNames.nextElement(); //获取相关的值
                Enumeration<String> values = request.getHeaders(name);
                while (values.hasMoreElements()) {//获取到当前元素
                    String value = values.nextElement();
                    requestTemplate.header(name, value);
                }
            }
        }
    }
}

订单模块

  • 表设计和接口设计
"订单编号的名字也要改 不能是以前的自增id 要变成order_no"
order_no是每一种商品的id
否则黑客早上下一单 晚上下一单 相减 就可以估计客流量 
分成了很详细的字段名
product_id/name/img 都是之前购买过的订单产生的数据

生成订单–用户下单

  • 入参
  • 从购物车中查询已经勾选的商品
  • 判断商品是否正在售卖中
  • 判断库存,保证不超卖
  • 调用商品服务扣库存 [及时更新库存]
  • 删除购物车为中对应的商品
  • 生成订单
  • 订单号生成规则
  • 循环保存每一个商品到order_item表

图片路径配置

com/imooc/cloud/mall/practice/cartorder/config/ImoocMallWebMvcConfig.java
package com.imooc.cloud.mall.practice.cartorder.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 描述:     地址映射配置
 */
@Configuration
public class ImoocMallWebMvcConfig implements WebMvcConfigurer {

    @Value("${file.upload.dir}")
    String FILE_UPLOAD_DIR;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/images/**").addResourceLocations("file:" + FILE_UPLOAD_DIR);
    }

购物车和订单模块-重难点

  • 订单表[多个关联(订单编号+状态)]、订单状态设计
  • 购物车流程
  • 下单流程
  • Feign调用的处理
购物车和订单模块-常见错误
  • Feign调用取不到User对象
  • URL错误拦截(图片url可以不拦截[订单+购物车])
  • 路由配置错误(二维码/图片不显示)